package com.caipos.actor.common.msg;

import com.caipos.actor.common.data.EntityType;
import com.caipos.actor.common.data.StringUtils;
import com.caipos.actor.common.data.id.CustomerId;
import com.caipos.actor.common.data.id.EntityId;
import com.caipos.actor.common.data.id.RuleChainId;
import com.caipos.actor.common.data.id.RuleNodeId;
import com.caipos.actor.common.data.msg.KbMsgCallback;
import com.caipos.actor.common.data.msg.KbMsgType;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.protobuf.ByteString;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.io.Serializable;
import java.util.Objects;
import java.util.UUID;

@Data
@Slf4j
public final class KbMsg implements Serializable {

    public static final String EMPTY_JSON_OBJECT = "{}";
    public static final String EMPTY_JSON_ARRAY = "[]";
    public static final String EMPTY_STRING = "";

    private final String queueName;
    private final String id;
    private final long ts;
    private final String type;
    private final KbMsgType internalType;
    private final EntityId originator;
    private final CustomerId customerId;
    private final KbMsgMetaData metaData;
    private final KbMsgDataType dataType;
    private final String data;
    private final RuleChainId ruleChainId;
    private final RuleNodeId ruleNodeId;

    private final UUID correlationId;
    private final Integer partition;

    @Getter(value = AccessLevel.NONE)
    @JsonIgnore
    //This field is not serialized because we use queues and there is no need to do it
    private final KbMsgProcessingCtx ctx;

    //This field is not serialized because we use queues and there is no need to do it
    @JsonIgnore
    transient private final KbMsgCallback callback;

    public static KbMsgBuilder newMsg() {
        return new KbMsgBuilder();
    }

    public KbMsgBuilder transform() {
        return new KbMsgTransformer(this);
    }

    public KbMsgBuilder copy() {
        return new KbMsgBuilder(this);
    }

    public KbMsg transform(String queueName) {
        return transform()
                .queueName(queueName)
                .resetRuleNodeId()
                .build();
    }

    // used for enqueueForTellNext
    public static KbMsg newMsg(KbMsg KbMsg, String queueName, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
        return KbMsg.transform()
                .id(UUID.randomUUID().toString())
                .queueName(queueName)
                .metaData(KbMsg.getMetaData())
                .ruleChainId(ruleChainId)
                .ruleNodeId(ruleNodeId)
                .callback(KbMsgCallback.EMPTY)
                .build();
    }

    public KbMsg copyWithNewCtx() {
        return copy()
                .ctx(ctx.copy())
                .callback(KbMsgCallback.EMPTY)
                .build();
    }

    private KbMsg(String queueName, String id, long ts, KbMsgType internalType, String type, EntityId originator, CustomerId customerId, KbMsgMetaData metaData, KbMsgDataType dataType, String data,
                  RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID correlationId, Integer partition, KbMsgProcessingCtx ctx, KbMsgCallback callback) {
        this.id = id != null ? id : UUID.randomUUID().toString();
        this.queueName = queueName;
        if (ts > 0) {
            this.ts = ts;
        } else {
            this.ts = System.currentTimeMillis();
        }
        this.internalType = internalType != null ? internalType : getInternalType(type);
        this.type = type != null ? type : this.internalType.name();
        this.originator = originator;
        if (customerId == null || customerId.isNullUid()) {
            if (originator != null && originator.getEntityType() == EntityType.CUSTOMER) {
                this.customerId = new CustomerId(originator.getId());
            } else {
                this.customerId = null;
            }
        } else {
            this.customerId = customerId;
        }
        this.metaData = metaData;
        this.dataType = dataType != null ? dataType : KbMsgDataType.JSON;
        this.data = data;
        this.ruleChainId = ruleChainId;
        this.ruleNodeId = ruleNodeId;
        this.correlationId = correlationId;
        this.partition = partition;
        this.ctx = ctx != null ? ctx : new KbMsgProcessingCtx();
        this.callback = Objects.requireNonNullElse(callback, KbMsgCallback.EMPTY);
    }

//    public static ByteString toByteString(KbMsg msg) {
//        return ByteString.copyFrom(toByteArray(msg));
//    }

//    public static byte[] toByteArray(KbMsg msg) {
//        MsgProtos.KbMsgProto.Builder builder = MsgProtos.KbMsgProto.newBuilder();
//        builder.setId(msg.getId().toString());
//        builder.setTs(msg.getTs());
//        builder.setType(msg.getType());
//        builder.setEntityType(msg.getOriginator().getEntityType().name());
//        builder.setEntityIdMSB(msg.getOriginator().getId().getMostSignificantBits());
//        builder.setEntityIdLSB(msg.getOriginator().getId().getLeastSignificantBits());
//
//        if (msg.getCustomerId() != null) {
//            builder.setCustomerIdMSB(msg.getCustomerId().getId().getMostSignificantBits());
//            builder.setCustomerIdLSB(msg.getCustomerId().getId().getLeastSignificantBits());
//        }
//
//        if (msg.getRuleChainId() != null) {
//            builder.setRuleChainIdMSB(msg.getRuleChainId().getId().getMostSignificantBits());
//            builder.setRuleChainIdLSB(msg.getRuleChainId().getId().getLeastSignificantBits());
//        }
//
//        if (msg.getRuleNodeId() != null) {
//            builder.setRuleNodeIdMSB(msg.getRuleNodeId().getId().getMostSignificantBits());
//            builder.setRuleNodeIdLSB(msg.getRuleNodeId().getId().getLeastSignificantBits());
//        }
//
//        if (msg.getMetaData() != null) {
//            builder.setMetaData(MsgProtos.KbMsgMetaDataProto.newBuilder().putAllData(msg.getMetaData().getData()).build());
//        }
//
//        builder.setDataType(msg.getDataType().ordinal());
//        builder.setData(msg.getData());
//
//        if (msg.getCorrelationId() != null) {
//            builder.setCorrelationIdMSB(msg.getCorrelationId().getMostSignificantBits());
//            builder.setCorrelationIdLSB(msg.getCorrelationId().getLeastSignificantBits());
//        }
//        if (msg.getPartition() != null) {
//            builder.setPartition(msg.getPartition());
//        }
//
//        builder.setCtx(msg.ctx.toProto());
//        return builder.build().toByteArray();
//    }

//    public static KbMsg fromBytes(String queueName, byte[] data, KbMsgCallback callback) {
//        try {
//            MsgProtos.KbMsgProto proto = MsgProtos.KbMsgProto.parseFrom(data);
//            KbMsgMetaData metaData = new KbMsgMetaData(proto.getMetaData().getDataMap());
//            EntityId entityId = EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB()));
//            CustomerId customerId = null;
//            RuleChainId ruleChainId = null;
//            RuleNodeId ruleNodeId = null;
//            UUID correlationId = null;
//            Integer partition = null;
//            if (proto.getCustomerIdMSB() != 0L && proto.getCustomerIdLSB() != 0L) {
//                customerId = new CustomerId(new UUID(proto.getCustomerIdMSB(), proto.getCustomerIdLSB()));
//            }
//            if (proto.getRuleChainIdMSB() != 0L && proto.getRuleChainIdLSB() != 0L) {
//                ruleChainId = new RuleChainId(new UUID(proto.getRuleChainIdMSB(), proto.getRuleChainIdLSB()));
//            }
//            if (proto.getRuleNodeIdMSB() != 0L && proto.getRuleNodeIdLSB() != 0L) {
//                ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB()));
//            }
//            if (proto.getCorrelationIdMSB() != 0L && proto.getCorrelationIdLSB() != 0L) {
//                correlationId = new UUID(proto.getCorrelationIdMSB(), proto.getCorrelationIdLSB());
//                partition = proto.getPartition();
//            }
//
//            KbMsgProcessingCtx ctx;
//            if (proto.hasCtx()) {
//                ctx = KbMsgProcessingCtx.fromProto(proto.getCtx());
//            } else {
//                // Backward compatibility with unprocessed messages fetched from queue after update.
//                ctx = new KbMsgProcessingCtx(proto.getRuleNodeExecCounter());
//            }
//
//            KbMsgDataType dataType = KbMsgDataType.values()[proto.getDataType()];
//            return new KbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), null, proto.getType(), entityId, customerId,
//                    metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, correlationId, partition, ctx, callback);
//        } catch (InvalidProtocolBufferException e) {
//            throw new IllegalStateException("Could not parse protobuf for KbMsg", e);
//        }
//    }

    public int getAndIncrementRuleNodeCounter() {
        return ctx.getAndIncrementRuleNodeCounter();
    }

    public KbMsgCallback getCallback() {
        // May be null in case of deserialization;
        return Objects.requireNonNullElse(callback, KbMsgCallback.EMPTY);
    }

//    public void pushToStack(RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
//        ctx.push(ruleChainId, ruleNodeId);
//    }
//
//    public KbMsgProcessingStackItem popFormStack() {
//        return ctx.pop();
//    }

    /**
     * Checks if the message is still valid for processing. May be invalid if the message pack is timed-out or canceled.
     * @return 'true' if message is valid for processing, 'false' otherwise.
     */
    public boolean isValid() {
        return getCallback().isMsgValid();
    }

    public long getMetaDataTs() {
        String tsStr = metaData.getValue("ts");
        if (!StringUtils.isEmpty(tsStr)) {
            try {
                return Long.parseLong(tsStr);
            } catch (NumberFormatException ignored) {
            }
        }
        return ts;
    }

    private KbMsgType getInternalType(String type) {
        if (type != null) {
            try {
                return KbMsgType.valueOf(type);
            } catch (IllegalArgumentException ignored) {
            }
        }
        return KbMsgType.NA;
    }

    public boolean isTypeOf(KbMsgType KbMsgType) {
        return internalType.equals(KbMsgType);
    }

    public boolean isTypeOneOf(KbMsgType... types) {
        for (KbMsgType type : types) {
            if (isTypeOf(type)) {
                return true;
            }
        }
        return false;
    }

    public static class KbMsgTransformer extends KbMsgBuilder {

        KbMsgTransformer(KbMsg KbMsg) {
            super(KbMsg);
        }

        /*
         * metadata is only copied if specified explicitly during transform
         * */
        @Override
        public KbMsgTransformer metaData(KbMsgMetaData metaData) {
            this.metaData = metaData.copy();
            return this;
        }

        /*
         * setting ruleNodeId to null when updating ruleChainId
         * */
        @Override
        public KbMsgBuilder ruleChainId(RuleChainId ruleChainId) {
            this.ruleChainId = ruleChainId;
            this.ruleNodeId = null;
            return this;
        }

        @Override
        public KbMsg build() {
            /*
             * always copying ctx when transforming
             * */
            if (this.ctx != null) {
                this.ctx = this.ctx.copy();
            }
            return super.build();
        }

    }

    public static class KbMsgBuilder {

        protected String queueName;
        protected String id;
        protected long ts;
        protected String type;
        protected KbMsgType internalType;
        protected EntityId originator;
        protected CustomerId customerId;
        protected KbMsgMetaData metaData;
        protected KbMsgDataType dataType;
        protected String data;
        protected RuleChainId ruleChainId;
        protected RuleNodeId ruleNodeId;
        protected UUID correlationId;
        protected Integer partition;
        protected KbMsgProcessingCtx ctx;
        protected KbMsgCallback callback;

        KbMsgBuilder() {}

        KbMsgBuilder(KbMsg KbMsg) {
            this.queueName = KbMsg.queueName;
            this.id = KbMsg.id;
            this.ts = KbMsg.ts;
            this.type = KbMsg.type;
            this.internalType = KbMsg.internalType;
            this.originator = KbMsg.originator;
            this.customerId = KbMsg.customerId;
            this.metaData = KbMsg.metaData;
            this.dataType = KbMsg.dataType;
            this.data = KbMsg.data;
            this.ruleChainId = KbMsg.ruleChainId;
            this.ruleNodeId = KbMsg.ruleNodeId;
            this.correlationId = KbMsg.correlationId;
            this.partition = KbMsg.partition;
            this.ctx = KbMsg.ctx;
            this.callback = KbMsg.callback;
        }

        public KbMsgBuilder queueName(String queueName) {
            this.queueName = queueName;
            return this;
        }

        public KbMsgBuilder id(String id) {
            this.id = id;
            return this;
        }

        public KbMsgBuilder ts(long ts) {
            this.ts = ts;
            return this;
        }

        /**
         * <p><strong>Deprecated:</strong> This should only be used when you need to specify a custom message type that doesn't exist in the {@link KbMsgType} enum.
         * Prefer using {@link #type(KbMsgType)} instead.
         *
         * */
        @Deprecated
        public KbMsgBuilder type(String type) {
            this.type = type;
            this.internalType = null;
            return this;
        }

        public KbMsgBuilder type(KbMsgType internalType) {
            this.internalType = internalType;
            this.type = internalType.name();
            return this;
        }

        public KbMsgBuilder originator(EntityId originator) {
            this.originator = originator;
            return this;
        }

        public KbMsgBuilder customerId(CustomerId customerId) {
            this.customerId = customerId;
            return this;
        }

        public KbMsgBuilder metaData(KbMsgMetaData metaData) {
            this.metaData = metaData;
            return this;
        }

        public KbMsgBuilder copyMetaData(KbMsgMetaData metaData) {
            this.metaData = metaData.copy();
            return this;
        }

        public KbMsgBuilder dataType(KbMsgDataType dataType) {
            this.dataType = dataType;
            return this;
        }

        public KbMsgBuilder data(String data) {
            this.data = data;
            return this;
        }

        public KbMsgBuilder ruleChainId(RuleChainId ruleChainId) {
            this.ruleChainId = ruleChainId;
            return this;
        }

        public KbMsgBuilder ruleNodeId(RuleNodeId ruleNodeId) {
            this.ruleNodeId = ruleNodeId;
            return this;
        }

        public KbMsgBuilder resetRuleNodeId() {
            return ruleNodeId(null);
        }

        public KbMsgBuilder correlationId(UUID correlationId) {
            this.correlationId = correlationId;
            return this;
        }

        public KbMsgBuilder partition(Integer partition) {
            this.partition = partition;
            return this;
        }

        public KbMsgBuilder ctx(KbMsgProcessingCtx ctx) {
            this.ctx = ctx;
            return this;
        }

        public KbMsgBuilder callback(KbMsgCallback callback) {
            this.callback = callback;
            return this;
        }

        public KbMsg build() {
            return new KbMsg(queueName, id, ts, internalType, type, originator, customerId, metaData, dataType, data, ruleChainId, ruleNodeId, correlationId, partition, ctx, callback);
        }

        public String toString() {
            return "KbMsg.KbMsgBuilder(queueName=" + this.queueName + ", id=" + this.id + ", ts=" + this.ts +
                    ", type=" + this.type + ", internalType=" + this.internalType + ", originator=" + this.originator +
                    ", customerId=" + this.customerId + ", metaData=" + this.metaData + ", dataType=" + this.dataType +
                    ", data=" + this.data + ", ruleChainId=" + this.ruleChainId + ", ruleNodeId=" + this.ruleNodeId +
                    ", correlationId=" + this.correlationId + ", partition=" + this.partition + ", ctx=" + this.ctx +
                    ", callback=" + this.callback + ")";
        }

    }
}
